/**************************************************************************\
    Copyright Microsoft Corporation. All Rights Reserved.
\**************************************************************************/

namespace Microsoft.ContactsBridge
{
    using Microsoft.ContactsBridge.Interop;
    using Standard;
    using System;
    using System.Collections.Generic;
    using System.Collections.Specialized;
    using System.IO;
    using System.Threading;
    using System.Windows.Threading;

    /// <summary>
    /// Manages the user's address book.
    /// </summary>
    /// <remarks>
    /// This class provides basic management functionality for the user's contacts.
    /// Contacts can be loaded by their ContactId string, or the entire address book
    /// can be enumerated.
    /// </remarks>
    public class ContactManager : DispatcherObject, IDisposable
    {
        #region Fields
        private const string _exceptionStringBadThreadId = "Contacts can only be created on STA threads and can only be accessed from the thread on which they were created.";

        private IContactManager _nativeManager;
        private ContactWatcher _watcher;
        private string _rootDirectory;
        #endregion

        #region Private Utilities

        /// <summary>
        /// Is the path considered to be part of the user's address book?
        /// </summary>
        /// <param name="path">The path to check for containment.  This can be a directory or a file name.</param>
        /// <returns>Whether this path is part of the user's address book.</returns>
        /// <remarks>
        /// This does not check for whether the path exists.
        /// </remarks>
        private bool _IsContainedPath(string path)
        {
            Assert.IsTrue(CheckAccess());
            if (!string.IsNullOrEmpty(path))
            {
                return path.StartsWith(RootDirectory);
            }
            return false;
        }

        /// <summary>
        /// Proxy for the CollectionChanged event that hides issues with re-entrancy and no listeners.
        /// </summary>
        /// <param name="sender">Sender of the event.</param>
        /// <param name="e">Event arguments.</param>
        internal void _NotifyCollectionChanged(object sender, ContactCollectionChangedEventArgs e)
        {
            Assert.IsTrue(CheckAccess());

            // By assigning to a temporary we avoid race conditions with the event losing listeners
            // after the null check, and ensure that we don't end up in any kind of weird cycle due
            // to re-entrancy.
            ContactCollectionChangedEventHandler handler = _CollectionChangedInternal;
            if (null != handler)
            {
                handler(sender, e);
            }
        }

        /// <summary>Try to create a contact from a file.  Swallow reasonable exceptions.</summary>
        /// <param name="filePath">The file to load from</param>
        /// <param name="contact">The contact to return</param>
        /// <returns></returns>
        private static bool _TryLoadContactFromFile(string filePath, out Contact contact)
        {
            contact = null;
            try
            {
                contact = new Contact(filePath);
                return true;
            }
            catch (FileNotFoundException) { }
            catch (InvalidDataException) { }
            catch (UnauthorizedAccessException) { }

            return false;
        }

        /// <summary>
        /// Perform basic validation to ensure that this object is in a valid state.
        /// This should be called at the beginning of any external-facing method.
        /// </summary>
        private void _Validate()
        {
            // DispatcherObject baseclass enforces that this object
            // is only accessed from the thread on which it was created.
            VerifyAccess();
            // All the constructors should catch this also.
            Assert.IsApartmentState(ApartmentState.STA);

            if (null == _nativeManager)
            {
                throw new ObjectDisposedException("this");
            }
        }

        internal HRESULT LoadNativeContact(string id, out IContact ppContact)
        {
            _Validate();
            return _nativeManager.Load(id, out ppContact);
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Create a new ContactManager.
        /// </summary>
        /// <remarks>
        /// This constructor roots the manager in the user's Contacts folder.
        /// </remarks>
        public ContactManager()
        {
            Validate.IsApartmentState(ApartmentState.STA, _exceptionStringBadThreadId);

            // This might throw, but I want that exception to propagate out.
            _nativeManager = new ContactManagerRcw();
            _nativeManager.Initialize("ContactsBridge", "1.0");

            _rootDirectory = ContactUtil.GetContactsFolder();

            // Delay initializing the _watcher until someone's actually listening for changes.
            // It's fairly expensive, no need to do it if the caller doesn't care.
        }

        #endregion

        #region Public Properties and Methods

        /// <summary>
        /// Load a Contact from a contactId string.
        /// </summary>
        /// <param name="contactId">The identifier for the contact to load.</param>
        /// <returns>
        /// The contact that maps to the provided identifier.
        /// </returns>
        /// <remarks>
        /// The full Contact ID string is a composite of different ways to identify a contact.
        /// All contacts contain a heirarchical "ContactID" property as a Guid.  Contacts
        /// that are backed by a file may also contain the path as part of the ID.
        /// These IDs can be retrieved from Contact's Id property.  Passing that Id to GetContact
        /// or TryGetContact will resolve to a Contact.  Usually it will resolve to the same contact
        /// as the one where the Id was gotten even if the contact has been updated, or the
        /// backing file has been renamed, copied, or moved.  If the contact has been deleted
        /// then this will try to find a compatible contact to load (e.g. if the file was copied
        /// before being deleted, it may find the copy), but if no contact can be found with
        /// the identifier then GetContact will throw an UnreachableContactException (whereas TryGetContact
        /// will simply return false).  If two contacts have been merged, then calling GetContact on
        /// the Id of one of the original contacts will generally load the merged one.<para/>
        /// This will not load contacts that are outside the user's address book.
        /// </remarks>
        /// <exception cref="UnreachableContactException">
        /// If the contactId doesn't resolve to a contact in the user's address book.
        /// </exception>
        public Contact GetContact(string contactId)
        {
            _Validate();
            Validate.NotNull(contactId, "contactId");

            IContact contact;
            HRESULT hr = _nativeManager.Load(contactId, out contact);
            if ((HRESULT)Win32Error.ERROR_NO_MATCH == hr)
            {
                throw new UnreachableContactException("The contact couldn't be loaded by this manager.");
            }
            hr.ThrowIfFailed("An unknown error occurred trying to load this contact.");

            string path;
            hr = ContactUtil.GetPath(contact, out path);
            hr.ThrowIfFailed("Unable to verify the path of the contact.");

            // CONSIDER: This Id might have resolved to a contact inside the address book if it was
            // lower resolution.  Should this drop any /PATH references in the ContactID and reload?
            if (!_IsContainedPath(path))
            {
                throw new UnreachableContactException("The contact couldn't be loaded by this manager because it identifies a contact outside the address book.");
            }

            return new Contact(this, contact);
        }

        /// <summary>
        /// Try to load a Contact from a contactId string.
        /// This returns a Boolean on failure rather than throw an exception for common errors.
        /// </summary>
        /// <param name="contactId">
        /// The identifier for the contact to load.
        /// </param>
        /// <param name="contact">
        /// When true is returned, this contains the contact that maps to the provided identifier.
        /// If false is returned this parameter is set to null.
        /// </param>
        /// <returns>
        /// Returns true if a contact could be found with the provided identifier.
        /// </returns>
        /// <remarks>
        /// The full Contact ID string is a composite of different ways to identify a contact.
        /// All contacts contain a heirarchical "ContactID" property as a Guid.  Contacts
        /// that are backed by a file may also contain the path as part of the ID.
        /// These IDs can be retrieved from Contact's Id property.  Passing that Id to GetContact
        /// or TryGetContact will resolve to a Contact.  Usually it will resolve to the same contact
        /// as the one where the Id was gotten even if the contact has been updated, or the
        /// backing file has been renamed, copied, or moved.  If the contact has been deleted
        /// then this will try to find a compatible contact to load (e.g. if the file was copied
        /// before being deleted, it may find the copy), but if no contact can be found with
        /// the identifier then GetContact will throw an UnreachableContactException (whereas TryGetContact
        /// will simply return false).  If two contacts have been merged, then calling GetContact on
        /// the Id of one of the original contacts will generally load the merged one.<para/>
        /// This will not load contacts that are outside the user's address book.
        /// </remarks>
        public bool TryGetContact(string contactId, out Contact contact)
        {
            contact = null;

            _Validate();
            Validate.NotNull(contactId, "contactId");

            IContact icontact;
            HRESULT hr = _nativeManager.Load(contactId, out icontact);
            if (hr.Succeeded())
            {
                string path;
                hr = ContactUtil.GetPath(icontact, out path);
                if (hr.Succeeded())
                {
                    // CONSIDER: This Id might have resolved to a contact inside the address book if it was
                    // lower resolution.  Should this drop any /PATH references in the ContactID and reload?
                    if (_IsContainedPath(path))
                    {
                        contact = new Contact(this, icontact);
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Create a new Contact associated with this manager.
        /// </summary>
        /// <param name="type">The type of contact to create.  Must be a valid enumeration value.</param>
        /// <returns>Returns a new Contact associated with this manager.</returns>
        /// <remarks>
        /// This is similar to creating a contact with Contact's constructor, except that CommitChanges
        /// will work despite that there is no file initially backing the contact.  After CommitChanges is
        /// called on the contact can later be loaded by Id with this manager.<para/>
        /// The contact is not considered to be part of this manager until it has been committed.
        /// </remarks>
        public Contact CreateContact()
        {
            return new Contact(this);
        }

        /// <summary>
        /// Add a Contact to this manager.
        /// </summary>
        /// <param name="contact">The contact to import.</param>
        /// <remarks>
        /// Add a contact that doesn't exist as part of this manager to its collection.
        /// </remarks>
        public void AddContact(Contact contact)
        {
            Validate.NotNull(contact, "contact");
            Contact.CopyToDirectory(contact, RootDirectory);
        }
        
        /// <summary>
        /// Remove a contact with the Id from this manager's collection.
        /// </summary>
        /// <param name="contactId">The id of the contact to remove.</param>
        /// <returns>
        /// Returns whether the manager's collection was modified as a result of this call.
        /// </returns>
        /// <remarks>
        /// Note that even though a contact has been removed the id may still resolve to a contact if called
        /// with ContactManager.Load (e.g. the file has been copied and pasted).
        /// </remarks>
        /// <exception cref="System.IO.IOException">
        /// This will throw if the file that backs the contact can't be deleted because of access restrictions.
        /// </exception>
        public bool Remove(string contactId)
        {
            Validate.NotNull(contactId, "contactId");

            Contact item;
            if (!TryGetContact(contactId, out item))
            {
                // Contact doesn't appear to be from this manager
                return false;
            }

            try
            {
                string path = item.Path;
                Assert.IsNotNull(path);
                if (File.Exists(path))
                {
                    // If the file is locked then it's a legitimate exception to throw here.
                    // MSDN says this doesn't throw if the file doesn't exist.
                    File.Delete(path);
                    return true;
                }
                return false;
            }
            finally
            {
                Utility.SafeDispose(ref item);
            }
        }

        /// <summary>
        /// Get a collection of all the contacts in this manager.
        /// </summary>
        /// <returns>
        /// </returns>
        public IEnumerable<Contact> GetContactCollection()
        {
            _Validate();

            IContactCollection coll = null;
            try
            {
                HRESULT hr = _nativeManager.GetContactCollection(out coll);
                hr.ThrowIfFailed("Unable to get a contact collection for this manager.");

                while (true)
                {
                    hr = coll.Next();
                    if (HRESULT.S_OK != hr)
                    {
                        break;
                    }

                    IContact contact;
                    coll.GetCurrent(out contact);
                    // Yielded Contact instance owns the native COM object.  Don't release it here.
                    // Ideally callers of the enumerator will call Dispose on the contact.
                    yield return new Contact(this, contact);
                }
                hr.ThrowIfFailed("An error occurred enumerating your contacts.");
            }
            finally
            {
                Utility.SafeRelease(ref coll);
            }
        }

        /// <summary>
        /// Get or set the contact that represents the user.
        /// </summary>
        /// <exception cref="UnreachableContactException">
        /// The Me contact can't be set to a contact that is not reachable by this ContactManager.
        /// This include contacts that are backed by files outside the user's address book.
        /// </exception>
        public Contact MeContact
        {
            get
            {
                _Validate();

                IContact contact;
                HRESULT hr = _nativeManager.GetMeContact(out contact);
                hr.ThrowIfFailed("Couldn't retrieve the Me contact.");
                if (null == contact)
                {
                    return null;
                }

                return new Contact(this, contact);
            }
            set
            {
                _Validate();
                Validate.NotNull(value, "value");

                HRESULT hr = HRESULT.S_OK;
                IContact contact = null;

                try
                {
                    // It's generally a logical error to set Me to a contact that can't be loaded (i.e. not committed to disk)
                    hr = _nativeManager.Load(value.Id, out contact);
                    if ((HRESULT)Win32Error.ERROR_NO_MATCH == hr)
                    {
                        throw new UnreachableContactException("The contact can't be loaded by this manager.");
                    }
                    hr.ThrowIfFailed("Error setting the Me contact.");

                    // If the contact is not under the Contacts path, then throw an argument exception.
                    // Arguably a contact that fails this condition shouldn't have been loaded by the ContactManager...
                    Assert.IsNotNull(value.Path);
                    if (!_IsContainedPath(value.Path))
                    {
                        throw new UnreachableContactException("Me can't be set to a contact outside the address book.");
                    }

                    hr = _nativeManager.SetMeContact(contact);
                    hr.ThrowIfFailed("Error setting the Me contact.");
                }
                finally
                {
                    Utility.SafeRelease(ref contact);
                }
            }
        }

        /// <summary>
        /// Get the user's Contacts folder.
        /// </summary>
        /// <remarks>
        /// The ContactManager considers all Contact files in and under this folder to be part of the user's
        /// address book.
        /// </remarks>
        public string RootDirectory
        {
            get
            {
                return _rootDirectory;
            }
        }

        #endregion

        #region IDisposable Pattern

        /// <summary>
        /// Release all resources held by this object.
        /// </summary>
        /// <remarks>
        /// Dispose can safely be called multiple times, but the object cannot be used for anything else.
        /// </remarks>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>Finalize the object.</summary>
        ~ContactManager()
        {
            Dispose(false);
        }

        /// <summary>
        /// Protected overload of Dispose that is standard in IDisposable patterns.
        /// </summary>
        /// <param name="disposing">Whether or not this is being called by Dispose, rather than by the finalizer.</param>
        /// <remarks>
        /// Overrides of this method should always call base.Dispose.
        /// </remarks>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                Utility.SafeRelease(ref _nativeManager);
                // This causes the tests to hang ... ? Not sure why.  Should re-enable this.
                //Utility.SafeDispose(ref _watcher);
            }
        }

        #endregion

        #region INotifyCollectionChanged Members

        private event ContactCollectionChangedEventHandler _CollectionChangedInternal;

        /// <summary>
        /// This event gets raised when there has been a change to the collection of contacts.
        /// </summary>
        public event ContactCollectionChangedEventHandler CollectionChanged
        {
            add
            { 
                // The act of subscribing to the event shouldn't really require thread affinity.
                if (null == _watcher)
                {
                    _watcher = new ContactWatcher(this);
                }
                _CollectionChangedInternal += value;
            }
            remove
            {
                _CollectionChangedInternal -= value;
            }
        }

        #endregion
    }
}
